package sk.tuke.lara.masplc.agents;

import jade.core.AID;
import jade.core.Agent;
import jade.core.behaviours.CyclicBehaviour;
import jade.domain.DFService;
import jade.domain.FIPAException;
import jade.domain.FIPAAgentManagement.DFAgentDescription;
import jade.domain.FIPAAgentManagement.ServiceDescription;
import jade.lang.acl.ACLMessage;
import jade.lang.acl.MessageTemplate;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.Set;

import org.jacop.constraints.Sum;
import org.jacop.core.Domain;
import org.jacop.core.IntVar;
import org.jacop.core.Store;
import org.jacop.search.DepthFirstSearch;
import org.jacop.search.IndomainMax;
import org.jacop.search.InputOrderSelect;
import org.jacop.search.Search;
import org.jacop.search.SelectChoicePoint;

import sk.tuke.lara.masplc.Conversations;
import sk.tuke.lara.masplc.Servicies;
import sk.tuke.lara.masplc.utils.CommunicationUtils;
import sk.tuke.lara.masplc.utils.ConstraintProcessor;
import sk.tuke.lara.masplc.utils.MsgTemplateFactory;

public class OperatorControlToBehaviour extends CyclicBehaviour {
    private static final long serialVersionUID = -8622005697846358022L;

    public static interface State {
        void process();
    }

    private State waitForControlIniciation;
    private State findPlc;
    private State requirementsFromAgents;
    private MessageTemplate requirementsMT;
    private State waitForRequirementsFromAgents;
    private State solveCco;
    private State setPlcs;
    private MessageTemplate setPlcMT;
    private State waitForAcceptsPlcs;

    /**
     * Koncovy stav vse v poradku
     */
    private State endSucces = new State() {
        @Override
        public void process() {
            System.out.println("$$$$ uspesne dokonceno $$$$");

            //tak a tady udelame reset a prepneme se zase na cekani na novy pozadavek
            initStates();
            changeState(this, waitForControlIniciation);
        }
    };

    /**
     * Koncovy stav, neco se pokazilo
     */
    private State endFault = new State() {
        @Override
        public void process() {
            System.err.println("Agent je ve stavy FAULT!");

            //tady to muze dal pokracovat a zalezi k jakemu faultu doslo
            initStates();
            changeState(this, waitForControlIniciation);
        }
    };
    private State actualState;
    private Set<String> controlledAgentsNames = new HashSet<String>();
    private List<PlcAgentDTO> recieverAgentsByPriority = new ArrayList<PlcAgentDTO>();
    private int pozadovanyCelkovyVykon = 0;


    public OperatorControlToBehaviour(Agent agent) {
        this.myAgent = agent;
        this.initStates();
    }

    private void initStates() {
        controlledAgentsNames.clear();
        recieverAgentsByPriority.clear();
        waitForControlIniciation = StateFactory.createWaitForControlInitiation(myAgent, this);
        findPlc = StateFactory.createFindPlc(myAgent, this);
        requirementsFromAgents = StateFactory.createRequirementsFromAgents(myAgent, this);
        waitForRequirementsFromAgents = StateFactory.createWaitForRequirements(myAgent, this);
        solveCco = StateFactory.createSolveCco(myAgent, this);
        setPlcs = StateFactory.createSetPlc(myAgent, this);
        waitForAcceptsPlcs = StateFactory.createWaitForSetPlc(myAgent, this);
        actualState = waitForControlIniciation;
    }

    @Override
    public void action() {
        System.out.println("processing " + actualState);
        actualState.process();
    }

    private void changeState(State from, State to) {
        // pripadne podminky pro prehod mezi jednotlivymi stavy
        if (to.equals(endFault)) {
            System.err.println("FAULT STATE setted");
        }
        this.actualState = to;
    }

    /**
     * Faktry trida na jednotlive stavy ktere potrebujeme k zivotu tohoto chovani
     * 
     * @author MAR
     */
    private static class StateFactory {

        /**
         * Nalzeni vsech PLC ktere se budou ucastnit vypoctu
         */
        public static State createFindPlc(final Agent myAgent, final OperatorControlToBehaviour behaviour) {
            return new State() {
                @Override
                public void process() {
                    behaviour.controlledAgentsNames.clear();
                    behaviour.recieverAgentsByPriority.clear();

                    DFAgentDescription template = new DFAgentDescription();
                    ServiceDescription sd = new ServiceDescription();
                    sd.setType(Servicies.FIRE_CONTROL_SERVICE);
                    template.addServices(sd);
                    try {
                        DFAgentDescription[] result = DFService.search(myAgent, template);
                        for (int i = 0; i < result.length; ++i) {
                            // TODO posun na platform name
                            String name = result[i].getName().getLocalName();
                            System.out.println("FIND names for HORAKY SERVICE = " + name);
                            behaviour.controlledAgentsNames.add(name);
                        }
                    } catch (FIPAException fe) {
                        fe.printStackTrace();
                    }

                    behaviour.changeState(this, behaviour.requirementsFromAgents);
                }
            };
        }

        /**
         * Nacteni omezeni od vsech agentu, rozeslani pozadavku
         */
        public static State createRequirementsFromAgents(final Agent myAgent, final OperatorControlToBehaviour behaviour) {
            return new State() {
                @Override
                public void process() {
                    // obeslat plc agenty at dodaji pozadavky
                    for (String recieverAgentName : behaviour.controlledAgentsNames) {
                        System.out.println("ask for requirements to " + recieverAgentName);

                        ACLMessage msg = new ACLMessage(ACLMessage.REQUEST);
                        msg.setConversationId(Conversations.REQUIREMENTS);
                        msg.addReceiver(new AID(recieverAgentName, AID.ISLOCALNAME));
                        myAgent.send(msg);
                    }

                    MsgTemplateFactory.matchConversiation(Conversations.REQUIREMENTS, myAgent.getAID());
                    behaviour.changeState(this, behaviour.waitForRequirementsFromAgents);
                }
            };
        }


        /**
         * Prijmout pozadavky o omezenich od agentu
         */
        public static State createWaitForRequirements(final Agent myAgent, final OperatorControlToBehaviour behaviour) {
            return new State() {
                private PriorityQueue<PlcAgentDTO> priorityQueue = new PriorityQueue<PlcAgentDTO>();
                private int msgRequirementsCount = 0;

                @Override
                public void process() {
                    ACLMessage msg = myAgent.receive(behaviour.requirementsMT);
                    if (msg != null) {
                        msgRequirementsCount++;
                        // Message received. Process it
                        System.out.println("Accept requirements from " + msg.getSender().toString());
                        PlcAgentDTO receivedInformations = CommunicationUtils.fromJson(msg.getContent(), PlcAgentDTO.class);
                        priorityQueue.add(receivedInformations);
                        if (msgRequirementsCount == behaviour.controlledAgentsNames.size()) {
                            // naskladat v poradi agenty do listu
                            behaviour.recieverAgentsByPriority.addAll(priorityQueue);
                            behaviour.changeState(this, behaviour.solveCco);
                        } else if (msgRequirementsCount > behaviour.controlledAgentsNames.size()) {
                            behaviour.changeState(this, behaviour.endFault);
                        }
                    } else {
                        // wait for others
                        behaviour.block();
                    }
                }
            };
        }

        /**
         * Vyreseni centralized constraint optimalization
         */
        public static State createSolveCco(final Agent myAgent, final OperatorControlToBehaviour b) {
            return new State() {
                Map<String, IntVar> intvarMap = new HashMap<String, IntVar>();
                ConstraintProcessor cp = new ConstraintProcessor(intvarMap);

                @Override
                public void process() {
                    intvarMap.clear();
                    Store store = new Store();

                    IntVar pozadovanyVykon = new IntVar(store, "Vykon", b.pozadovanyCelkovyVykon, b.pozadovanyCelkovyVykon);

                    IntVar[] horaky = new IntVar[b.recieverAgentsByPriority.size()];
                    List<ConstraintsDTO> constraintsForProccess = new ArrayList<>();

                    for (int i = 0; i < horaky.length; i++) {
                        PlcAgentDTO plcAgentDto = b.recieverAgentsByPriority.get(i);
                        horaky[i] = new IntVar(store, plcAgentDto.getName(), 0, plcAgentDto.getMaxVykon());
                        intvarMap.put(plcAgentDto.getName(), horaky[i]);
                        // local agent constraints
                        for (ConstraintsDTO constraint : plcAgentDto.getConstraints()) {
                            System.out.println("addtitional constraints " + constraint + " from " + plcAgentDto.getName());
                            constraintsForProccess.add(constraint);
                        }
                    }
                    // zpracovat vsechny omezeni
                    for (ConstraintsDTO constraintsDTO : constraintsForProccess) {
                        store.impose(cp.processConstraint(constraintsDTO));
                    }
                    // our global constraint
                    store.impose(new Sum(horaky, pozadovanyVykon)); // soucet vsech musi dat nas celk. vykon

                    // prvni kontrola nekonzistence omezeni
                    if (!store.consistency()) {
                        b.changeState(this, b.endFault);
                        System.err.println("not consistent constraints");
                    }

                    // hledame reseni
                    Search<IntVar> label = new DepthFirstSearch<IntVar>();
                    SelectChoicePoint<IntVar> select = new InputOrderSelect<IntVar>(store, horaky, new IndomainMax<IntVar>());
                    boolean existsSolution = label.labeling(store, select);
                    if (existsSolution) {
                        Domain[] solution = label.getSolution();
                        for (int i = 0; i < solution.length; i++) {
                            Integer reseni = Integer.valueOf(solution[i].toString());
                            b.recieverAgentsByPriority.get(i).setPozadovanyVykon(reseni);
                            System.out.println(horaky[i].id + " - " + reseni);
                        }
                        // po uspesnem vyreseni
                        b.changeState(this, b.setPlcs);
                    } else {
                        // pri neexistujicim reseni
                        b.changeState(this, b.endFault);
                        System.err.println("no solution");
                    }

                }
            };
        }

        /**
         * Nastavi aktualne rozpocitane hodnoty na plc
         */
        public static State createSetPlc(final Agent myAgent, final OperatorControlToBehaviour b) {
            return new State() {
                @Override
                public void process() {

                    for (PlcAgentDTO recieverAgent : b.recieverAgentsByPriority) {
                        System.out.println("send to " + recieverAgent.getName() + " set TO W = "
                                + recieverAgent.getPozadovanyVykon());

                        ACLMessage msg = new ACLMessage(ACLMessage.REQUEST);
                        msg.setConversationId(Conversations.SET_PLC);
                        msg.setContent(CommunicationUtils.toJson(recieverAgent));
                        msg.addReceiver(new AID(recieverAgent.getName(), AID.ISLOCALNAME));
                        myAgent.send(msg);
                    }
                    // wait for msgs.
                    b.setPlcMT = MsgTemplateFactory.matchConversiation(Conversations.SET_PLC, myAgent.getAID());
                    b.changeState(this, b.waitForAcceptsPlcs);
                }
            };
        }

        /**
         * Pocka a zkontroluje nastaveni jednotlivych PLC na lokalni pozadovanou hodnotu
         */
        public static State createWaitForSetPlc(final Agent myAgent, final OperatorControlToBehaviour b) {
            return new State() {
                private long msgSetPlcCount = 0;

                @Override
                public void process() {
                    ACLMessage msg = myAgent.receive(b.setPlcMT);
                    if (msg != null) {
                        msgSetPlcCount++;
                        // Message received. Process it
                        System.out.println("Accept setvalue from " + msg.getSender().toString());
                        if (msgSetPlcCount == b.recieverAgentsByPriority.size()) {
                            b.changeState(this, b.endSucces);
                        } else if (msgSetPlcCount > b.recieverAgentsByPriority.size()) {
                            b.changeState(this, b.endFault);
                        }
                    } else {
                        // wait for others
                        b.block();
                    }
                }
            };
        }

        /**
         * Ceka na zpravu na pozadavek na nastaveni POZADOVANEHO VYKONU
         */
        public static State createWaitForControlInitiation(final Agent myAgent, final OperatorControlToBehaviour behaviour) {
            return new State() {
                @Override
                public void process() {
                    ACLMessage msg = myAgent.receive(MsgTemplateFactory.matchConversiation(
                            Conversations.INITIATE_CONTROL, myAgent.getAID()));
                    if (msg != null) {
                        behaviour.pozadovanyCelkovyVykon = Integer.parseInt(msg.getContent());
                        System.out.println(" START GROUP CONTROL to pozadovany celk. vykon = "
                                + behaviour.pozadovanyCelkovyVykon
                                + " [W]");
                        behaviour.changeState(this, behaviour.findPlc);
                    } else {
                        // wait for others
                        behaviour.block();
                    }
                }
            };
        }
    }
}
